package com.unitd.frame.mybatis.datasource;

import com.alibaba.druid.pool.DruidDataSource;
import com.unitd.frame.comm.utils.StringUtils;
import com.unitd.frame.spring.SpringInstanceFactory;
import com.unitd.frame.spring.SpringInstanceProvider;
import com.unitd.frame.spring.prop.ResourceUtils;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.lang3.Validate;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.jdbc.datasource.lookup.DataSourceLookup;
import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;

/**
 * @desc 自动路由多数据源（读写分离 和 水平分库路由）
 * @filename MutiRouteDataSource.java
 * @copyright www.unitd.com
 * @author Hudan
 * @version 1.0
 * @date 2017/2/17
 */
public class MutiRouteDataSource extends AbstractDataSource implements ApplicationContextAware, InitializingBean, EnvironmentAware {
	private static final String MASTER_KEY = "master";

	/** Spring的上下文对象 */
	private ApplicationContext context;
	/** 当前Spring应用环境 */
	private Environment environment;
	/** 默认的数据库连接池类型,默认为druid */
	private DataSourceTypeEnum dataSourceType = DataSourceTypeEnum.druid;
	/** Spring提供的用于查找数据源接口,她提供根据名字匹配查找数据源的功能! 用的最多的是在多数据源中根据name配置 */
	private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
	/** 默认数据源 */
	private DataSource defaultDataSource;
	/** 通过配置文件定义的数据源集合 */
	private Map<Object, DataSource> targetDataSources;
	/** 数据库分库组数,默认一组 */
	private int dbGroupNums = 1;

	/**
	 * @desc 重写AbstractDataSource的getConnection方法,从目标数据源中获取数据库连接
	 * @return Connection
	 */
	@Override
	public Connection getConnection() throws SQLException {
		return determineTargetDataSource().getConnection();
	}

	@Override
	public Connection getConnection(String username, String password) throws SQLException {
		return determineTargetDataSource().getConnection(username, password);
	}

	/**
	 * @desc 实现ApplicationContextAware接口, 重写设置Spring上下文对象
	 * @param applicationContext Spring上下文对象
	 */
	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.context = applicationContext;
		SpringInstanceFactory.setSpringInstanceProvider(new SpringInstanceProvider(context));
	}

	/**
	 * @desc 实现InitializingBean接口, 重写读取properties属性文件后的处理方法
	 */
	@Override
	public void afterPropertiesSet() {
		try {
			dataSourceType = DataSourceTypeEnum.valueOf(ResourceUtils.getProperty("db.dataSourceType", DataSourceTypeEnum.druid.name()));
		} catch (Exception e) {
			throw new IllegalArgumentException("Property 'db.dataSourceType' expect:" + Arrays.toString(DataSourceTypeEnum.values()));
		}

		// 解析配置,得到数据源信息
		Map<String, DataSourceInfo> map = parseDataSourceConfFromProperties();

		if(map.isEmpty()) {
			throw new RuntimeException("没有找到数据库配置信息,请确认!");
		}

		// 根据DataSource创建bean并注册到容器中
		registerDataSources(map);

		if (this.targetDataSources == null || targetDataSources.isEmpty()) {
			throw new IllegalArgumentException("properties配置文件中没有定义目标数据源,请确认!");
		}
		if (this.defaultDataSource == null) {
			throw new IllegalArgumentException("properties配置文件中没有定义默认的数据源,请确认!");
		}
	}

	@Override
	public void setEnvironment(Environment environment) {
		this.environment = environment;
	}

	/**
	 * @desc 在进行DAO操作前,通过上下文环境变量,获得数据源对象
	 * @return DataSource 确定的目标数据源
	 */
	protected DataSource determineTargetDataSource() {
		Object lookupKey = determineCurrentLookupKey();
		if (lookupKey == null) {
			return defaultDataSource;
		}
		DataSource dataSource = targetDataSources.get(lookupKey);
		if (dataSource == null) {
			throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
		}
		return dataSource;
	}

	/**
	 * @desc 在进行DAO操作前,通过上下文环境变量,获得数据源对象的key
	 * @return DataSourceKey
	 */
	protected Object determineCurrentLookupKey() {
		return DataSourceContextHolder.get().getDataSourceKey();
	}

	/**
	 * @desc 解析配置,得到数据源Map
	 * @return 数据源Map
	 */
	private Map<String, DataSourceInfo> parseDataSourceConfFromProperties() {
		// 属性文件
		Map<String, DataSourceInfo> mapDataSource = new HashMap<>(1);

		dbGroupNums = Integer.parseInt(getProperty("db.group.size", "1"));
		logger.info(">>>>>>dbGroupNums:" + dbGroupNums);
		for (int i = 0; i < dbGroupNums; i++) {
			String groupPrefix = i == 0 ? "" : "group" + i;
			String datasourceKey = (StringUtils.isNotBlank(groupPrefix) ? groupPrefix + "." : "") + MASTER_KEY;
			DataSourceInfo sourceInfo = new DataSourceInfo(i, datasourceKey);
			mapDataSource.put(datasourceKey, sourceInfo);

			//解析同组下面的slave
			int index = 1;
			while (true) {
				datasourceKey = (StringUtils.isNotBlank(groupPrefix) ? groupPrefix + "." : "") + "slave" + index;
				if (!containsProperty(datasourceKey + ".db.url")) break;
				sourceInfo = new DataSourceInfo(i, datasourceKey);
				mapDataSource.put(datasourceKey, sourceInfo);
				index++;
			}
		}
		return mapDataSource;
	}

	/**
	 * @desc 根据DataSource创建bean并注册到容器中
	 * @param mapCustom DataSource对象信息
	 */
	private void registerDataSources(Map<String, DataSourceInfo> mapCustom) {

		DefaultListableBeanFactory acf = (DefaultListableBeanFactory) this.context.getAutowireCapableBeanFactory();
		Iterator<String> iter = mapCustom.keySet().iterator();

		Map<Object, DataSource> targetDataSources = new HashMap<>(1);

		BeanDefinitionBuilder beanDefinitionBuilder;
		while (iter.hasNext()) {
			String dsKey = iter.next();
			DataSourceInfo dataSourceInfo = mapCustom.get(dsKey);
			//如果当前库为最新一组数据库,注册beanName为master
			logger.info(">>>>>begin to initialize datasource：" + dsKey + "\n================\n" + dataSourceInfo.toString() + "\n==============");

			if (DataSourceTypeEnum.druid == dataSourceType) {
				beanDefinitionBuilder = DruidDataSourceBuilder.builder(dataSourceInfo);
			} else if (DataSourceTypeEnum.hikariCP == dataSourceType) {
				beanDefinitionBuilder = HikariDataSourceBuilder.builder(dataSourceInfo);
			} else {
				beanDefinitionBuilder = DruidDataSourceBuilder.builder(dataSourceInfo);
			}

			acf.registerBeanDefinition(dsKey, beanDefinitionBuilder.getRawBeanDefinition());

			DataSource ds = (DataSource) this.context.getBean(dsKey);
			targetDataSources.put(dsKey, ds);
			// 设置默认数据源
			if (dataSourceInfo.dbGroupIndex == dbGroupNums - 1) {
				defaultDataSource = ds;
			}
			logger.info("bean[" + dsKey + "] has initialized! lookupKey:" + dsKey);

			// 创建数据源对象bean并注册到容器中
			DataSourceContextHolder.get().registerDataSourceKey(dsKey);
		}

		// 将目标数据源对象加入数据源对象池中
		addTargetDataSources(targetDataSources);
	}

	/**
	 * @desc 将目标数据源对象加入数据源对象池中
	 * @param targetDataSources 目标数据源池对象
	 */
	public void addTargetDataSources(Map<Object, DataSource> targetDataSources) {
		if (this.targetDataSources == null) {
			this.targetDataSources = targetDataSources;
		} else {
			this.targetDataSources.putAll(targetDataSources);
		}
	}

	/**
	 * @desc 设置数据源查找
	 * @param dataSourceLookup
	 */
	public void setDataSourceLookup(DataSourceLookup dataSourceLookup) {
		this.dataSourceLookup = (dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup());
	}
	protected Object resolveSpecifiedLookupKey(Object lookupKey) {
		return lookupKey;
	}

	protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
		if (dataSource instanceof DataSource) {
			return (DataSource) dataSource;
		} else if (dataSource instanceof String) {
			return this.dataSourceLookup.getDataSource((String) dataSource);
		} else {
			throw new IllegalArgumentException(
					"Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
		}
	}

	/**
	 * @desc 获取属性配置信息
	 * @param key 属性配置key
	 * @param defs 默认值
	 * @return String
	 */
	private String getProperty(String key, String... defs) {
		String value;
		String defValue = defs != null && defs.length > 0 && defs[0] != null ? defs[0] : null;
		value = environment.getProperty(key);
		if (StringUtils.isBlank(value)) {
			value = ResourceUtils.getProperty(key);
		}
		return StringUtils.isBlank(value) ? defValue : value;
	}

	private boolean containsProperty(String key) {
		return environment.containsProperty(key) || StringUtils.isNotBlank(ResourceUtils.getProperty(key));
	}

	/**
	 * @desc 数据源配置信息对象
	 * 它与mysql.properties等类似的数据源配置文件信息中的配置信息进行映射和读取、解析等
	 * @filename DataSourceInfo.java
	 * @copyright www.unitd.com
	 * @author Hudan
	 * @version 1.0
	 * @date 2017/2/16
	 */
	private class DataSourceInfo {
		/* ****************** 全局数据源配置信息 ****************** */
		/** 数据库类型(mysql/oracle/sqlserver等,默认mysql) */
		protected String dbType = "MySQL";
		/** 数据库驱动 */
		protected String driveClassName;
		/** 初始数据库连接池大小 */
		protected int initialSize;
		/** 连接池最小连接量 */
		protected int minIdle;
		/** 最大活跃连接数 */
		protected int maxActive;
		/** 获取连接等待超时的时间 */
		protected long maxWait;
		/** 一个连接在池中最小生存的时间(单位:毫秒) */
		protected long minEvictableIdleTimeMillis;
		/** 多久才进行一次检测,检测需要关闭的空闲连接, 即心跳检测(单位:毫秒) */
		protected long timeBetweenEvictionRunsMillis;
		/** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能 */
		protected boolean testOnBorrow;
		/** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能 */
		protected boolean testOnReturn;
		/** 建议配置为true,不影响性能,并且保证安全性.申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效 */
		protected boolean testWhileIdle;

		/* ****************** 私有数据源的数据库连接配置信息 ****************** */
		/** 数据库连接URL */
		protected String connUrl;
		/** 数据库连接用户 */
		protected String userName;
		/** 数据库连接密码 */
		protected String password;

		/* ****************** 数据库集群、master/slaver、分库时的配置信息 ****************** */
		/** 数据库集群时,主数据库的键 */
		protected static final String MASTER_KEY = "master";
		protected boolean master;
		/** 当使用分库功能时,分库的组序列(分库ID) */
		protected int dbGroupIndex;

		/**
		 * @desc 构造方法,根据配置信息,初始化数据源信息
		 * @param groupIndex 当使用分库功能时,分库的组序列
		 * @param keyPrefix key的前缀
		 */
		public DataSourceInfo(int groupIndex, String keyPrefix) {
			String tmpVal;
			this.dbGroupIndex = groupIndex;

			/* ****************** 数据源的全局配置信息 ******************  */
			this.dbType = getProperty("db.type", "mysql");
			this.driveClassName = getProperty("db.driverClass");
			this.initialSize = Integer.parseInt(getProperty("db.initialSize", "1"));
			this.minIdle = Integer.parseInt(getProperty("db.minIdle", "1"));
			this.maxActive = Integer.parseInt(getProperty("db.maxActive", "10"));
			this.maxWait = Integer.parseInt(getProperty("db.maxWait", "60000"));
			this.minEvictableIdleTimeMillis = Integer.parseInt(getProperty("db.minEvictableIdleTimeMillis", "300000"));
			this.timeBetweenEvictionRunsMillis = Integer.parseInt(getProperty("db.timeBetweenEvictionRunsMillis", "60000"));
			this.testOnBorrow = Boolean.parseBoolean(getProperty("db.testOnBorrow", "false"));
			this.testOnReturn = Boolean.parseBoolean(getProperty("db.testOnReturn", "false"));
			this.testWhileIdle = Boolean.parseBoolean(getProperty("db.testWhileIdle", "true"));

			/* ****************** 数据源的私有配置信息 ******************  */
			// 主库配置信息
			this.master = keyPrefix.contains(MASTER_KEY);
			// 数据库连接URL
			this.connUrl = getProperty(keyPrefix + ".db.url");
			Validate.notBlank(this.connUrl, "Config [%s.db.url] is required", keyPrefix);
			// 数据库用户名
			this.userName = getProperty(keyPrefix + ".db.username");
			Validate.notBlank(this.userName, "Config [%s.db.username] is required", keyPrefix);
			// 数据库用户密码
			this.password = getProperty(keyPrefix + ".db.password");
			Validate.notBlank(this.password, "Config [%s.db.password] is required", keyPrefix);

			/* 当私有配置下进行了相关配置时,将覆盖全局配置 */
			if (!StringUtils.isNull((tmpVal = getProperty(keyPrefix + ".db.initialSize")))) {
				this.initialSize = Integer.parseInt(tmpVal);
			}
			if (!StringUtils.isNull((tmpVal = getProperty(keyPrefix + ".db.minIdle")))) {
				this.minIdle = Integer.parseInt(tmpVal);
			}
			if (!StringUtils.isNull((tmpVal = getProperty(keyPrefix + ".db.maxActive")))) {
				this.maxActive = Integer.parseInt(tmpVal);
			}
			if (!StringUtils.isNull((tmpVal = getProperty(keyPrefix + ".db.minEvictableIdleTimeMillis")))) {
				this.minEvictableIdleTimeMillis = Integer.parseInt(tmpVal);
			}
			if (!StringUtils.isNull((tmpVal = getProperty(keyPrefix + ".db.minEvictableIdleTimeMillis")))) {
				this.minEvictableIdleTimeMillis = Integer.parseInt(tmpVal);
			}
			if (!StringUtils.isNull((tmpVal = getProperty(keyPrefix + ".db.timeBetweenEvictionRunsMillis")))) {
				this.timeBetweenEvictionRunsMillis = Integer.parseInt(tmpVal);
			}
			if (!StringUtils.isNull((tmpVal = getProperty(keyPrefix + ".db.testOnBorrow")))) {
				this.testOnBorrow = Boolean.parseBoolean(tmpVal);
			}
			if(!StringUtils.isNull((tmpVal = getProperty(keyPrefix + ".db.testOnReturn")) )) {
				this.testOnReturn = Boolean.parseBoolean(tmpVal);
			}
			if(!StringUtils.isNull((tmpVal =getProperty(keyPrefix +".db.testWhileIdle")))){
				this.testWhileIdle = Boolean.parseBoolean(tmpVal);
			}
		}

		@Override
		public String toString() {
			StringBuffer str = new StringBuffer();
			str.append("dbGroupIndex").append(" = ").append(dbGroupIndex).append("\n");
			str.append("role").append(" = ").append(master ? "master" : "slave").append("\n");
			str.append("driveClassName").append(" = ").append(driveClassName).append("\n");
			str.append("connUrl").append(" = ").append(connUrl).append("\n");
			str.append("userName").append(" = ").append(userName);
			return str.toString();
		}
	}

	/**
	 * @desc 初始化durid数据库连接池
	 * @filename DruidDataSourceBuilder.java
	 * @copyright www.unitd.com
	 * @author Hudan
	 * @version 1.0
	 * @date 2017/12/5
	 */
	private static class DruidDataSourceBuilder {

		/**
		 * @desc 通过Spring的 genericBeanDefinition 生成阿里巴巴的Druid数据源对象信息集合,类似用在Spring的配置文件中增加bean标签进行数据源定义
		 * Spring的genericBeanDefinition相关源码说明：
		 * RootBeanDefinition,ChildBeanDefinition,GenericBeanDefinition均继承了AbstractBeanDefiniton,
		 * 其中BeanDefinition是配置文件<bean>元素标签在容器中内部表示形式.
		 * <bean>元素标签拥有class、scope、lazy-init等配置属性,BeanDefinition则提供了相应的beanClass、scope、lazyInit属性,BeanDefinition和<bean>中的属性是一一对应的.
		 * 其中RootBeanDefinition是最常用的实现类,它对应一般性的<bean>元素标签,GenericBeanDefinition是自2.5以后新加入的bean文件配置属性定义类,是一站式服务类.
		 * 在配置文件中可以定义父<bean>和子<bean>,父<bean>用RootBeanDefinition表示,而子<bean>用ChildBeanDefiniton表示,而没有父<bean>的<bean>就使用RootBeanDefinition表示.
		 * AbstractBeanDefinition对两者共同的类信息进行抽象.
		 * Spring通过BeanDefinition将配置文件中的<bean>配置信息转换为容器的内部表示,并将这些BeanDefiniton注册到BeanDefinitonRegistry中.
		 * Spring容器的BeanDefinitionRegistry就像是Spring配置信息的内存数据库,主要是以map的形式保存,后续操作直接从BeanDefinitionRegistry中读取配置信息.
		 * 一般情况下,BeanDefinition只在容器启动时加载并解析,除非容器刷新或重启,这些信息不会发生变化,当然如果用户有特殊的需求,也可以通过编程的方式在运行期调整BeanDefinition的定义.
		 */
		public static BeanDefinitionBuilder builder(DataSourceInfo dataSourceInfo) {

			BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(DruidDataSource.class);
			beanDefinitionBuilder.addPropertyValue("driverClassName", dataSourceInfo.driveClassName);
			beanDefinitionBuilder.addPropertyValue("url", dataSourceInfo.connUrl);
			beanDefinitionBuilder.addPropertyValue("username", dataSourceInfo.userName);
			beanDefinitionBuilder.addPropertyValue("password", dataSourceInfo.password);
			beanDefinitionBuilder.addPropertyValue("testWhileIdle", true);
			if("mysql".equals(dataSourceInfo.dbType.toLowerCase())) {
				beanDefinitionBuilder.addPropertyValue("validationQuery", "SELECT 'x'"); 				// 验证sql(Mysql)
			} else if("oracle".equals(dataSourceInfo.dbType.toLowerCase())) {
				beanDefinitionBuilder.addPropertyValue("validationQuery", "SELECT 'x' FROM DUAL"); 		// 验证sql(Oracle)
			} else {
				beanDefinitionBuilder.addPropertyValue("validationQuery", "SELECT 'x'"); 				// 验证sql
			}

			if (dataSourceInfo.initialSize > 0) {
				beanDefinitionBuilder.addPropertyValue("initialSize", dataSourceInfo.initialSize);
			}
			if (dataSourceInfo.maxActive > 0) {
				beanDefinitionBuilder.addPropertyValue("maxActive", dataSourceInfo.maxActive);
			}
			if (dataSourceInfo.minIdle > 0) {
				beanDefinitionBuilder.addPropertyValue("minIdle", dataSourceInfo.minIdle);
			}
			if (dataSourceInfo.maxWait > 0) {
				beanDefinitionBuilder.addPropertyValue("maxWait", dataSourceInfo.maxWait);
			}
			if (dataSourceInfo.minEvictableIdleTimeMillis > 0) {
				beanDefinitionBuilder.addPropertyValue("minEvictableIdleTimeMillis", dataSourceInfo.minEvictableIdleTimeMillis);
			}
			if (dataSourceInfo.timeBetweenEvictionRunsMillis > 0) {
				beanDefinitionBuilder.addPropertyValue("timeBetweenEvictionRunsMillis", dataSourceInfo.timeBetweenEvictionRunsMillis);
			}
			if (dataSourceInfo.maxWait > 0) {
				beanDefinitionBuilder.addPropertyValue("maxWait", dataSourceInfo.maxWait);
			}

			beanDefinitionBuilder.addPropertyValue("testOnBorrow", dataSourceInfo.testOnBorrow);
			beanDefinitionBuilder.addPropertyValue("testOnReturn", dataSourceInfo.testOnReturn);

			return beanDefinitionBuilder;
		}
	}

	/**
	 * @desc 初始化hikari数据库连接池
	 * @filename HikariDataSourceBuilder.java
	 * @copyright www.unitd.com
	 * @author Hudan
	 * @version 1.0
	 * @date 2017/12/5
	 */
	private static class HikariDataSourceBuilder {

		public static BeanDefinitionBuilder builder(DataSourceInfo dataSourceInfo) {

			BeanDefinitionBuilder beanDefinitionBuilder =  BeanDefinitionBuilder.genericBeanDefinition(HikariDataSource.class);

			beanDefinitionBuilder.addPropertyValue("driverClassName", dataSourceInfo.driveClassName);
			beanDefinitionBuilder.addPropertyValue("jdbcUrl", dataSourceInfo.connUrl);
			beanDefinitionBuilder.addPropertyValue("username", dataSourceInfo.userName);
			beanDefinitionBuilder.addPropertyValue("password", dataSourceInfo.password);
			if("mysql".equals(dataSourceInfo.dbType.toLowerCase())) {
				beanDefinitionBuilder.addPropertyValue("connectionTestQuery", "SELECT 'x'"); 				// 验证sql(Mysql)
			} else if("oracle".equals(dataSourceInfo.dbType.toLowerCase())) {
				beanDefinitionBuilder.addPropertyValue("connectionTestQuery", "SELECT 'x' FROM DUAL"); 		// 验证sql(Oracle)
			} else {
				beanDefinitionBuilder.addPropertyValue("connectionTestQuery", "SELECT 'x'"); 				// 验证sql
			}

			if (dataSourceInfo.timeBetweenEvictionRunsMillis > 0) {
				beanDefinitionBuilder.addPropertyValue("connectionTimeout", dataSourceInfo.timeBetweenEvictionRunsMillis);
			}
			if (dataSourceInfo.minIdle > 0) {
				beanDefinitionBuilder.addPropertyValue("idleTimeout", dataSourceInfo.minIdle);
			}
			if (dataSourceInfo.maxWait > 0) {
				beanDefinitionBuilder.addPropertyValue("maxLifetime", dataSourceInfo.maxWait);
			}

			return beanDefinitionBuilder;
		}
	}
}
